package com.uxsino.commons.db.aggregate;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.druid.util.StringUtils;

/**
 * an OLAP cube style aggregation sql generator. deal with multiple, nested dimension data 
 *
 *
 */
public class Cube {

    private String srcTableName;

    private static class DimensionRef {

        public String joinType;

        public String leftTable;

        public String leftField;

        public String alias;

        public IDimension dimension;

    }

    private Map<String, Measure> measures = new LinkedHashMap<>();

    private List<DimensionRef> dimensions = new ArrayList<>();

    /**
     * create a cube
     * @param tableName main table name
     */
    public Cube(String tableName) {
        this.srcTableName = tableName;
    }

    /**
     * generate aggregate sql like SELECT ... FROM ... JOIN ... WHERE ... GROUP BY 
     * @param whereClause
     * @param groupByClause
     * @return
     */
    public String createAggrSql(String whereClause, String groupByClause) {
        StringBuilder sql = new StringBuilder();
        sql.append("SELECT ");

        boolean commaNeeded = false;
        for (Measure measure : measures.values()) {
            if (commaNeeded) {
                sql.append(",");
            }
            sql.append(measure.toSql());

            commaNeeded = true;
        }

        sql.append("\nfrom ");

        sql.append(srcTableName);
        sql.append('\n');

        for (DimensionRef ref : dimensions) {
            IDimension dim = ref.dimension;
            sql.append(ref.joinType);
            sql.append(" JOIN ");
            if (!dim.isSimpleTable()) {
                sql.append('(');
            }
            sql.append(dim.getSrcExpr());
            if (!dim.isSimpleTable()) {
                sql.append(')');
            }
            if (!StringUtils.isEmpty(ref.alias)) {
                sql.append(' ');
                sql.append(ref.alias);
                sql.append(' ');
            }
            sql.append(" ON ");
            if (!StringUtils.isEmpty(ref.leftTable)) {
                sql.append(ref.leftTable);
                sql.append('.');
            }
            sql.append(ref.leftField);
            sql.append(" = ");
            sql.append(ref.alias);
            sql.append(".");
            sql.append(dim.getPrimaryKey());
            sql.append("\n");
        }

        if (!StringUtils.isEmpty(whereClause)) {
            sql.append(" WHERE ");
            sql.append(whereClause);
            sql.append('\n');
        }
        sql.append("GROUP BY\n");
        sql.append(groupByClause);
        return sql.toString();
    }

    /**
     * add a dimension
     * 
     * @param leftExpr which field to join this dimension table with, like x JOIN y ON x.leftExpr=y.primaryKey
     * @param dimension
     */
    public void addDimension(String leftExpr, IDimension dimension) {
        addDimension(null, leftExpr, "INNER", dimension, null);
    }

    /**
     * add a dimension. dimensions must be added in order of joins. if a dimension joins a table of another dimension, the referred
     * dimension must be added first, or an IllegalArgumentException will be thrown
     * 
     * @param leftTable the table to join on, use the main table if it is null or empty
     * @param leftExpr which field to join this dimension table with, like x JOIN y ON leftTable.leftExpr=y.primaryKey
     * @param joinType LEFT, INNER, RIGHT, etc. null or empty as INNER by default
     * @param dimension
     * @param alias table alias
     */
    public void addDimension(String leftTable, String leftExpr, String joinType, IDimension dimension, String alias) {
        DimensionRef ref = new DimensionRef();
        ref.leftTable = leftTable;
        ref.dimension = dimension;
        ref.leftField = leftExpr;
        ref.joinType = joinType;

        if (StringUtils.isEmpty(alias)) {
            if (dimension.isSimpleTable()) {
                ref.alias = dimension.getSrcExpr();
            } else {
                throw new IllegalArgumentException("an alias must be set for none simple table dimension");
            }
        } else {
            ref.alias = alias;
        }
        if ((!StringUtils.isEmpty(ref.leftTable)) && !hasDimensionTable(ref.leftTable)) {
            throw new IllegalArgumentException("left table " + ref.leftTable + " doesn't exist");
        }
        dimensions.add(ref);
    }

    private boolean hasDimensionTable(String table) {
        for (DimensionRef ref : dimensions) {
            if (ref.alias.equals(table))
                return true;
        }
        return false;
    }

    /**
     * add a measure. 
     * @param name the output field name: select ... as name
     * @param measure
     */
    public void addMeasure(String name, Measure measure) {
        measures.put(name, measure);
    }

    /** 
     * get all output fields
     * 
     * @return
     */
    public Collection<String> getOutputFields() {
        return new ArrayList<String>(measures.keySet());
    }

    // /**
    // * all the table used
    // * @return
    // */
    // public Collection<String> getTableNames() {
    // Set<String> result = new HashSet<>();
    // result.add(srcTableName);
    // dimensions.forEach(ref -> result.addAll(ref.dimension.getSourceTables()));
    // return result;
    // }
}